Lecture 4

Topics to be covered today :

1. List Comprehensions
2. Dictionaries

List Comprehensions

List comprehensions in Python is a efficient and concise technique to transform one iterable to
another.

From for loops to list comprehension

Structure of a basic list comprehension : [new_value for value in list]

Examples :


In [2]:
# For loop
a = [1, 2, 3, 4, 5]

b = []
for i in a:
    b.append(i**2)
print(b)

###############################################

# Using list comprehension

c = [value ** 2 for value in a]
print(c)


[1, 4, 9, 16, 25]
[1, 4, 9, 16, 25]

The new iterable needn't be a list


In [8]:
c = tuple(value ** 2 for value in a)
d = set(value **2 for value in a)
print(c)
print(d)


(1, 4, 9, 16, 25)
{16, 1, 4, 9, 25}

To be technically correct, the above two are NOT comprehensions but something called generator expressions. But for all practical use you can think of them as unofficial tuple and set comprehension.

Using conditional in list comprehension


In [17]:
a = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
b = []

for value in a:
    if value % 2 == 1:
        b.append(value)
print(b)

##############################################

c = [value for value in a if value % 2 == 1]

print(c)


[1, 1, 3, 5, 13, 21, 55, 89]
[1, 1, 3, 5, 13, 21, 55, 89]

So other than it's elegance why do we need list comprehensions ? It's cause they are faster than their for loop variants. Run the next cell and look at the time difference. You'll most probably see that the comprehension is nearly twice as fast as the doubly nested loop.


In [2]:
import timeit

programs = {
    "Loop : " : """
result = []
for i in range(200):
    result.append(i * 2)
""",
    "List Comprehension : " : 'result = [i * 2 for i in range(200)]',
}

for technique, code in programs.items():
    print(technique, timeit.Timer(stmt=code).timeit())


Loop :  15.112850335000076
List Comprehension :  7.895465900999625

List Comprehension for Nested Loops


In [24]:
b = [i ** 2 for i in range(0, 4) for j in range(0, 5)]
print(b)


[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 4, 4, 4, 4, 4, 9, 9, 9, 9, 9]

Rookie Mistake

Let us consider the list comprehension above. A common mistake is to write the inner loops before it's encapsulating loop. Like so :


In [23]:
c = [i ** 2 for j in range(0, 5) for i in range(0, 4)]
print(c)


[0, 1, 4, 9, 0, 1, 4, 9, 0, 1, 4, 9, 0, 1, 4, 9, 0, 1, 4, 9]

So, remember --> Left to right, outer to inner..........


In [27]:
d = [i + j ** 2 + k ** 3 for i in range(0, 5) for j in range(0, i) for k in range(0, j)]
print(d)


[3, 4, 7, 8, 5, 8, 9, 13, 14, 21]

I would strongly encourage you to think directly in terms of list comprehension rather than constructing the nested loop structure first and then convert to comprehension.

Also, the new_value can be anything Python type. It need not maintain the type of the list from which the new one is being constructed. Let us look at how the star program can be done via comprehension.


In [7]:
a = [['*' for j in range(0, i)] for i in range(1, 6)]

for sublist in a:
    for j in sublist:
        print(j, end='')
    print()


*
**
***
****
*****

So, why keep loops at all ?

It's cause when a bunch of statements are to be executed in an iterative construct then it maybe impossible to do in a list comprehension. List comprehension/Generator Expression is for creating a new iterable from an existing one. What if you need to print stuff, do something with list elements that doesn't involve making a new iterable, etc ?

Then list comprehension doesn't make sense cause that's not what it was designed for.

Readability

 It's important to make sure that your code is readable and able to be understood by other developers. We will elaborate on this later on whilst talking about PEP8 and good development practices. 

In [29]:
a = [i * 2 for i in range(0, 5) if i % 2 == 1]

b = [
    i * 2
    for i in range(0, 5)
    if i % 2 == 1
]

##############################################
c = [i + j ** 2 + k ** 3 for i in range(0, 5) for j in range(0, i) for k in range(0, j)]

d = [
    i + j ** 2 + k ** 3
    for i in range(0, 5)
    for j in range(0, i)
    for k in range(0, j)
]

print(a)
print(b)
print(c)
print(d)


[2, 6]
[2, 6]
[3, 4, 7, 8, 5, 8, 9, 13, 14, 21]
[3, 4, 7, 8, 5, 8, 9, 13, 14, 21]

Dictionaries

Dictionaries in Python, are like Hash Maps or Associative Arrays. It is an iterable of key-value pairs. Obviously it is a mutable type.

So, why can't we instead have a list of tuples where every tuple is (key, value) ?

That's because the search/indexing operation will be really slow, that is linear time. Whereas for dictionary it is ideally constant time.

Construction


In [58]:
# Empty dictionary
dict1 = {}

# Comma separated list
dict2 = {"one" : 1, "two" : 2, "three" : 3, "four" : 4, "five" : 5}

# Using constructor(arguments as key = value pairs)
dict3 = dict(one=1, two=2, three=3, four=4, five=5)

# Using nested iterables
dict4 = dict([("one", 1), ("two", 2), ("three", 3), ("four", 4), ("five", 5)])
dict5 = dict([["one", 1], ["two", 2], ["three", 3], ["four", 4], ["five", 5]])

# Using separate iterables
dict6 = dict(zip(["one", "two", "three", "four", "five"], [1, 2, 3, 4, 5]))

print(dict1)
print(dict2)
print(dict3)
print(dict4)
print(dict5)
print(dict6)


{}
{'four': 4, 'three': 3, 'five': 5, 'two': 2, 'one': 1}
{'four': 4, 'three': 3, 'five': 5, 'two': 2, 'one': 1}
{'four': 4, 'three': 3, 'five': 5, 'two': 2, 'one': 1}
{'four': 4, 'three': 3, 'five': 5, 'two': 2, 'one': 1}
{'four': 4, 'three': 3, 'five': 5, 'two': 2, 'one': 1}

You can see that dictionaries aren't sorted by key or value.

Searching in Dictionaries


In [65]:
value1 = dict6['five']
print(value1)

# Throws an error. Better to use get() method
value1 = dict6['five']
print(value1)


5
5

In [67]:
value2 = dict6.get("six", None)
print(value2)
print(type(value2))


None
<class 'NoneType'>

Various methods exist for dictionaries :

  1. get()
  2. items()
  3. copy()
  4. del .......

Look at this link for more : https://jeffknupp.com/blog/2015/08/30/python-dictionaries/

Short in-class Assignment

Construct Pascal's triangle for a given number of lines. Just print a space between numbers, don't worry about other spaces. The main construction of the values per line should be done via list comprehension.

For example

Input : 7

Output :

1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1


In [73]:
from math import factorial

height = 7
lst = [[int(factorial(n) / (factorial(k) * factorial(n - k))) for k in range(n+1)]
  for n in range(height)]

for sublist in lst:
    for value in sublist:
        print(value, end=' ')
    print()


1 
1 1 
1 2 1 
1 3 3 1 
1 4 6 4 1 
1 5 10 10 5 1 
1 6 15 20 15 6 1 

In [1]:
[["x" for j in range(0, i)] for i in range(0, 5)]


Out[1]:
[[], ['x'], ['x', 'x'], ['x', 'x', 'x'], ['x', 'x', 'x', 'x']]

In [4]:
a0 = 0
a1 = 1

print(a0)
print(a1)
for i in range(0, 5):
    print(a0 + a1)
    temp = a1
    a1 = a0 + a1
    a0 = temp


0
1
1
2
3
5
8

In [ ]: